Skip to content

feat(huey): Add an option to monitor Huey periodic tasks - #8052

Draft
wedamija wants to merge 2 commits into
masterfrom
danf/huey-crons
Draft

wedamija wants to merge 2 commits into
masterfrom
danf/huey-crons

Conversation

@wedamija

@wedamija wedamija commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Part of a series adding automatic Crons monitoring for the most-used job schedulers in each SDK. Check-ins for a monitor that doesn't exist yet are dropped, so today each monitor has to be created by hand with its schedule copied from the code. Sending the job's schedule as the monitor config lets Sentry create the monitor and keep it in sync.

This adds monitor_periodic_tasks to HueyIntegration, matching Celery's monitor_beat_tasks. When it's on, each @huey.periodic_task run sends check-ins with its crontab(...) schedule in UTC, so Sentry creates the monitor on the first run. The slug is the task name. It's off by default, and regular tasks are unaffected.

sentry_sdk.init(
    integrations=[HueyIntegration(monitor_periodic_tasks=True, exclude_periodic_tasks=["cleanup"])],
)

Tasks check in without a config when they use a custom validate_datetime, restrict both day and day_of_week, or run on a Huey instance with utc=False.

Docs: getsentry/sentry-docs#20057

Send check-ins around each run of a Huey periodic task when
HueyIntegration(monitor_periodic_tasks=True) is set. Huey crontab()
schedules are sent as the monitor config when Huey runs in UTC, so
Sentry creates the monitor and keeps it in sync.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Results 📊

✅ 133821 passed | ⏭️ 7293 skipped | Total: 141114 | Pass Rate: 94.83% | Execution Time: 448m 55s

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +2917
Passed Tests 📈 +2958
Failed Tests 📉 -58
Skipped Tests 📈 +17

➖ Removed Tests (58)

View removed tests
  • test_input_attributes_nonstreaming_chat[False-True-False-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-True-False-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-True-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-True-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-True-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-True-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-False-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-False-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-False-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat[False-False-False-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-True-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-True-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-True-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-True-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-False-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-False-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-False-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-True-False-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-True-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-True-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-True-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-True-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-False-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-False-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-False-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[True-False-False-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-True-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-True-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-True-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-True-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-False-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-False-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-False-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-True-False-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-True-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-True-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-True-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-True-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-False-messages0-expected_system_instructions0-expected_input_messages0]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-False-messages1-expected_system_instructions1-expected_input_messages1]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-False-messages2-expected_system_instructions2-expected_input_messages2]
    • File: tests.integrations.mistral.test_mistral
  • test_input_attributes_nonstreaming_chat_async[False-False-False-messages3-expected_system_instructions3-expected_input_messages3]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[True-True-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[True-True-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[True-False-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[True-False-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[False-True-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[False-True-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[False-False-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat[False-False-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[True-True-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[True-True-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[True-False-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[True-False-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[False-True-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[False-True-False]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[False-False-True]
    • File: tests.integrations.mistral.test_mistral
  • test_output_attributes_nonstreaming_chat_async[False-False-False]
    • File: tests.integrations.mistral.test_mistral

All tests are passing successfully.

✅ Patch coverage is 96.61% (57 of 59 changed executable lines covered; target 80%).
Project statement coverage is 90.27% (up 0.02 percentage points from base (0f7acd5) to head (191a0c7)).

Changed files with executable lines (1)
File Patch coverage Changed executable lines
sentry_sdk/integrations/huey.py 96.61% 57/59 covered; missed: 314, 344; partial branches: 313, 343
Coverage diff
@@            Coverage Diff             @@
##        master     #8052       +/-##
==========================================
+ Coverage    90.25%    90.27%    +0.02%
==========================================
  Files          202       202         —
  Tracked lines     26660     26720       +60
  Branches      9926      9952       +26
==========================================
+ Hits         24062     24121       +59
- Misses        2598      2599        +1
- Partials      1510      1511        +1

Generated by Coverage Action

Comment thread sentry_sdk/integrations/huey.py
Comment on lines +63 to +71
_run_periodic_tasks(huey)

check_ins = _check_ins(envelopes)
assert [c["status"] for c in check_ins] == ["in_progress", "error"]
assert {c["monitor_slug"] for c in check_ins} == {"nightly"}

events = [envelope.get_event() for envelope in envelopes]
(error,) = [e for e in events if e is not None and e.get("exception")]
assert error["exception"]["values"][0]["type"] == "ValueError"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error-path test lets Huey's re-raised exception escape

test_periodic_task_error calls _run_periodic_tasks() without handling the task's ValueError. Huey's execution path re-raises task failures, so the test exits before checking the error check-in or captured event. Catch the expected ValueError (for example, with pytest.raises) around the helper call, then run the assertions.

Evidence
  • test_periodic_task_error defines failing() to raise ValueError and calls _run_periodic_tasks(huey) directly before its assertions.
  • _run_periodic_tasks() calls huey.execute(task) without catching exceptions.
  • _wrap_task_execute() in sentry_sdk/integrations/huey.py captures task exceptions and re-raises them; the existing execute_huey_task() test helper also catches exceptions around huey.execute(task) for failing tasks.
  • The monitor decorator records the error check-in but does not suppress the exception, so the call aborts before the new test's check-in and event assertions.

Identified by Warden · code-review · BCZ-G9S

Comment thread sentry_sdk/integrations/huey.py
_CRONTAB_CODE = crontab().__code__


def _get_closure_var(func: "Any", name: str) -> "Any":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructing the monitor from code inspection like this feels brittle. Is there any other way we can get to it, e.g. by monkeypatching something (the crontab func maybe? haven't looked into it so don't know if feasible)?

@wedamija wedamija changed the title feat(huey): Add cron monitoring for Huey periodic tasks feat(huey): Add an option to monitor Huey periodic tasks Oct 20, 2026
`RetryTask`, `TaskLockedException` and `CancelExecution` aren't failures
for Huey or for the integration's error capture, so they no longer mark
the check-in as an error.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment on lines +75 to +93
@pytest.mark.parametrize("exception", [CancelExecution, RetryTask, TaskLockedException])
def test_periodic_task_control_flow(sentry_init, capture_envelopes, exception):
sentry_init(integrations=[HueyIntegration(monitor_periodic_tasks=True)])
envelopes = capture_envelopes()
huey = MemoryHuey()

calls = []

@huey.periodic_task(crontab())
def cleanup():
if not calls:
calls.append(1)
raise exception()

_run_periodic_tasks(huey)

statuses = [c["status"] for c in _check_ins(envelopes)]
assert statuses[:2] == ["in_progress", "ok"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RetryTask is reported as a successful periodic run

Have you considered treating RetryTask differently from cancellation and lock control flow? _monitor_periodic_task catches it before the monitor context exits, so the check-in is marked ok even though Huey re-enqueues the task and the work has not finished. The retry runs under a new check-in, which can make one scheduled firing appear as multiple successful runs; Celery's beat-cron retry handler marks its check-in error instead.

Evidence
  • _monitor_periodic_task wraps each execution in monitor, whose context manager chooses ok when it exits without an active exception.
  • The handler catches RetryTask inside that context and re-raises it afterward, so the first attempt's check-in is marked ok before Huey handles the retry.
  • Huey re-enqueues a task after RetryTask (test_task_retry in tests/integrations/huey/test_huey.py verifies a queued retry); the wrapper starts a new check-in for that execution.
  • Celery's parallel crons_task_retry handler marks a retried beat-task check-in error.

Identified by Warden · code-review · GQU-RC3

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants